#include "readMSH.hpp"
#include "streamAccess.hpp"
#include "exceptions.hpp"
#include "magic_enum.hpp"
#include "timer.hpp"
#include "logger.hpp"
#include <iomanip>


namespace hms::gmsh {

/* all available section names are stated here, so that a warning can be
 * generated. The MSH file format states that unknown sections are ignored,
 * which is respected here. However, this should only apply to section which
 * are ignored by gmsh as well.
 * */
enum class Section {
	/* implemented: */
	MeshFormat,
	PhysicalNames,
	Entities,
	Nodes,
	Elements,
	/* not implemented: */
	PartitionedEntities,
	Periodic,
	GhostElements,
	Parametrizations,
	NodeData,
	ElementData,
	ElementNodeData,
	InterpolationScheme,
};

MSH readFile( std::istream& file ){

	std::shared_ptr<log::logger> logger { log::getLogger() };
	logger->trace("Reading MSH file...");
	
	MSH msh;
	std::string line;
	std::optional<Section> section;
	bool sectionEnded {true};
	
	while ( std::getline(file, line) ){
		/* to avoid copies */
		std::string_view lineView {line};
		/* expect a section or section end, which are indicated by the dollar 
		 * symbol */
		if ( line[0] == '$' ){
			/* check first if it's the end of a section. If so, it must be the
			 * end of the current section */
			if ( lineView.substr(1, 3) == "End" ) {
				auto endSection {
					magic_enum::enum_cast<Section>( lineView.substr(4) )
				};
				if (
					endSection.has_value() &&
					endSection.value() == section.value()
				){
					sectionEnded = true;
					continue;
				}
				throw hms::ParseError(
					"Section end mismatch! Expected \"$End" +
					std::string{ magic_enum::enum_name( section.value() ) } +
					"\", got \"" + line + "\""
				);
			}
			/* if it's not the end of a section, it's the beginning of one.
			 * First check, whether the previous section has been properly
			 * ended, then proceed dependent on the exact section */
			if ( !sectionEnded )
				throw hms::ParseError(
					"Encountered \"" + line +
					"\" before end of previous section \"" +
					std::string{ magic_enum::enum_name( section.value() ) } +
					"\"!"
				);
			/* only a certain selection of sections can be handled. This is
			 * implicitly implemented by the selection of enums.
			 * */
			section = magic_enum::enum_cast<Section>( lineView.substr(1) );
			if ( section.has_value() ) {
				sectionEnded = false;
				switch ( section.value() ) {
					case Section::MeshFormat:
						readMeshFormat(file, msh);
						break;
					case Section::PhysicalNames:
						readPhysicalNames(file, msh);
						break;
					case Section::Entities:
						readEntities(file, msh);
						break;
					case Section::Nodes:
						readNodes(file, msh);
						break;
					case Section::Elements:
						readElements(file, msh);
						break;
					default:
						hms::warning( "MSH file contains section \"" +
							std::string{magic_enum::enum_name(section.value())}
							+
							"\", which is not implemented in gmsh importer!"
						);
				}
			}
		}
	}
	logger->trace(
		"Found"
		"\n{0}{1} points"
		"\n{0}{2} curves"
		"\n{0}{3} nodes"
		"\n{0}{4} node blocks"
		"\n{0}{5} element blocks"
		"\n{0}{6} physical names",
		indent(),
		msh.points.size(),
		msh.curves.size(),
		msh.nNodes,
		msh.nodeBlocks.size(),
		msh.elementBlocks.size(),
		msh.physicalNames.size()
	);

	checkValidity(msh);

	return msh;
}


void readMeshFormat   ( std::istream& file, MSH& msh){
	/* mesh format section only has one line */
	std::string line;
	std::getline( file, line );
	std::stringstream converter {line};
	
	converter >> msh.version;
	if ( msh.version < minMeshVersion )
		throw hms::ParseError(
			"Mesh format version is " + std::to_string(msh.version) +
			", minimum version required is " + std::to_string(minMeshVersion) +
			"!" );
	
	Index fileType;
	converter >> fileType;
	msh.isBinary = ( fileType == 1 );
	if (msh.isBinary)
		throw hms::ParseError(
			"Handling of binary mesh output currently not implemented. "
			"Please choose ASCII output to generate MSH file."
		);
	converter >> msh.dataSize;
}
	
void readPhysicalNames( std::istream& file, MSH& msh ){
	/* first line is just the number of physical names */
	std::string line;
	std::getline( file, line );
	std::stringstream converter {line};
	
	Index nPhysicalNames;
	converter >> nPhysicalNames;
	converter.clear();
	
	/* initialising the physical names vector with a size does not make 
	 * a lot of sense, because the struct has a string in it, i.e. a dynamic
	 * component */
	Index dim;
	Index tag;
	std::string name;
	
	for (Index i {0}; i<nPhysicalNames; ++i){
		std::getline(file, line);
		converter.str(line);
		converter.clear();
		/* remove quotes around name */
		converter >> dim >> tag >> std::quoted(name);
		msh.physicalNames.push_back(
			{ static_cast<Dim>(dim), tag, std::move(name) }
		);
	}
}

void readEntities( std::istream& file, MSH& msh){
	/* Entities are ordered in the MSH file format as follows:
	 * points, curves, surfaces, volumes.
	 * Volumes are ignored, because they don't have meaning in 2D space.
	 * Surfaces are ignored, because there can be only one and nothing is gained
	 * from storing it.
	 * Otherwise, this routine follows the file structure, i.e. first, the
	 * numbers of all entity types are read, then points, then curves. */
	using hms::read;
	
	/* first line is the number of entities of each type */
	std::string line;
	std::getline(file, line);
	std::stringstream converter {line};
	
	Index nPoints, nCurves, nSurfaces; /* volumes are ignored */
	converter >> nPoints >> nCurves >> nSurfaces;
	
	if (nSurfaces>1)
		throw hms::ParseError(
			"Dealing with multiple surfaces is currently not implemented in "
			"gmsh import tool"
		);
	
	Index tag;
	Index nPhysicalTags;
	
	Vector3s coords;
	Vector3s min;
	Vector3s max;
	Index nBoundingPoints;
	
	for ( Index i{0}; i<nPoints+nCurves; ++i ){
		std::getline(file, line);
		converter.str(line);
		converter.clear();
		/* both points and curves start with a tag */
		converter >> tag;
		/* one set of coordinates for points, two for curves */
		if (i<nPoints)
			read(converter, coords);
		else {
			read(converter, min);
			read(converter, max);
		}
		
		/* physical tags are laid out the same for both */
		converter >> nPhysicalTags;
		IdxVector physicalTags (nPhysicalTags);
		read(converter, physicalTags);
		
		/* points have no further entries */
		if (i<nPoints)
			msh.points.push_back( {
				tag, std::move(physicalTags), std::move(coords)
			} );
		else {
		/* curves still have bounding points */
			converter >> nBoundingPoints;
			IdxVector pointTags (nBoundingPoints);
			read(converter, pointTags);
			msh.curves.push_back( {
				tag, std::move(physicalTags),
				std::move(min), std::move(max),
				std::move(pointTags)
			} );
		}
			
	}
}

void readNodes( std::istream& file, MSH& msh){
	using hms::read;
	
	/* first line is the number of blocks, nodes, and min/max of tags */
	std::string line;
	std::getline(file, line);
	std::stringstream converter {line};
	
	Index nEntityBlocks, nNodes, minNodeTag, maxNodeTag;
	converter >> nEntityBlocks >> nNodes >> minNodeTag >> maxNodeTag;
	
	msh.nNodes = nNodes;
	msh.nodeBlocks.reserve( nEntityBlocks );
	
	Index entityDim, entityTag;
	bool isParametric;
	Index nNodesInBlock;
	
	for ( Index i{0}; i<nEntityBlocks; ++i ){
		std::getline(file, line);
		converter.str(line);
		converter.clear();
		converter >> entityDim >> entityTag >> isParametric >> nNodesInBlock;
		
		IdxVector nodeTags (nNodesInBlock);
		Matrix3Xs coords (3, nNodesInBlock);
		
		/* the tags come before the coordinates, one per line */
		for ( Index j{0}; j<nNodesInBlock; ++j ){
			std::getline(file, line);
			converter.str(line);
			converter.clear();
			converter >> nodeTags[j];
		}
		for ( Index j{0}; j<nNodesInBlock; ++j ){
			std::getline(file, line);
			converter.str(line);
			converter.clear();
			coords.col(j) = read<Vector3s>(converter);
		}
		/* write node blocks to MSH */
		msh.nodeBlocks.push_back( {
			static_cast<Dim>(entityDim),
			entityTag,
			std::move(nodeTags),
			std::move(coords)
		} );
	}
}

Index nNodeTags( ElementType et ){
	switch(et){
		case ElementType::point:
			return 1;
		case ElementType::line:
			return 2;
		case ElementType::triangle:
			return 3;
		case ElementType::quadrilateral:
			return 4;
		default:
			throw hms::ParseError("ElementType not implemented in nNodeTags()!");
	}
}
	
void readElements( std::istream& file, MSH& msh){
	using hms::read;
	std::string line;
	std::getline(file, line);
	std::stringstream converter {line};
	
	Index nEntityBlocks, nElements, minElementTag, maxElementTag;
	converter >> nEntityBlocks >> nElements >> minElementTag >> maxElementTag;
	
	Index entityDim, entityTag, elementType;
	Index nElementsInBlock;
	
	for ( Index i{0}; i<nEntityBlocks; ++i ){
		std::getline(file, line);
		converter.str(line);
		converter.clear();
		
		converter >> entityDim >> entityTag >> elementType >> nElementsInBlock;
		
		IdxVector elementTags (nElementsInBlock);
		ArrayXXi nodeTags (
			/* each element type needs a specific amount of nodes. A point needs
			 * one, a line needs two, etc. That determines the row count */
			nNodeTags( static_cast<ElementType>(elementType) ),
			nElementsInBlock
		);
		/* the element tag and the respective node tags are on one line.
		 * This may change in future versions, as it changed for nodes before.
		 **/
		for ( Index j{0}; j<nElementsInBlock; ++j ){
			std::getline(file, line);
			converter.str(line);
			converter.clear();
			
			converter >> elementTags[j];
			nodeTags.col(j) = read<ArrayXi>( converter, nodeTags.rows() );
		}
		msh.elementBlocks.push_back( {
			static_cast<Dim>(entityDim),
			entityTag,
			static_cast<ElementType>(elementType),
			std::move(elementTags),
			std::move(nodeTags)
		} );
	}
}

void checkValidity( MSH& msh ){

	msh.isValid = true;
	std::shared_ptr<log::logger> logger { log::getLogger() };

	auto checkAndReport = [&](
		size_t size,
		std::string_view name,
		log::Level logLevel = log::Level::warn
	){
		if ( size == 0 ){
			if ( logLevel >= log::Level::err )
				msh.isValid = false;
			logger->log(logLevel, "MSH file: no {} found!", name);
		}
	};
	checkAndReport( msh.nNodes              , "nodes"     , log::Level::err );
	checkAndReport( msh.nodeBlocks.size()   , "nodeBlocks", log::Level::err );
	checkAndReport( msh.elementBlocks.size(), "cells"     , log::Level::err );
	checkAndReport( msh.points.size()       , "points" );
	checkAndReport( msh.curves.size()       , "curves" );
	checkAndReport( msh.physicalNames.size(), "boundary patches" );
}


}